/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.common.base.util;

import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ZipUtil;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * hutool工具类拓展
 *
 * @author wangmm@ketr.com.cn
 * @date 2019/9/11
 */
public class ZipUtilExt extends ZipUtil {

    /**
     * 目录分隔符
     */
    private static final String SEPARATOR = "/";

    /**
     * 解压异常信息
     */
    private static final String EXCEPTION_MALFORMED = "MALFORMED";

    /**
     * GBK
     */
    private static final Charset GBK = Charset.forName("GBK");

    /**
     * UTF_8
     */
    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    /**
     * 临时文件缓存位置
     */
    private static String TEMP_FOLDER = null;

    /**
     * 对流中的数据加入到压缩文件，自动关闭输入流
     * 路径列表和流列表长度必须一致
     *
     * @param paths 流数据在压缩文件中的路径或文件名
     * @param ins   路径对应的文件流(文件夹对应的流为null)
     * @return 压缩后的文件流
     * @throws UtilException 异常
     */
    public static ByteArrayInputStream zip(List<String> paths, List<? extends InputStream> ins) throws UtilException, IOException  {
        return zip(paths, ins, null);
    }

    /**
     * 对流中的数据加入到压缩文件，自动关闭输入流
     * 路径列表和流列表长度必须一致
     *
     * @param paths   流数据在压缩文件中的路径或文件名
     * @param ins     路径对应的文件流(文件夹对应的流为null)
     * @param charset 编码
     * @return 压缩后的文件流
     * @throws UtilException 异常
     */
    public static ByteArrayInputStream zip(List<String> paths, List<? extends InputStream> ins, Charset charset) throws UtilException, IOException {

        if (charset == null) {
            charset = UTF_8;
        }
        if (paths.size() != ins.size()) {
            throw new IllegalArgumentException("Path length is not equals to InputStream length !");
        }

        //转换为map
        Map<String, InputStream> fileMap = new HashMap<>();
        for (int i = 0; i < paths.size(); i++) {
            fileMap.put(paths.get(i), ins.get(i));
        }

        return zip(fileMap, charset);
    }

    /**
     * zip打包
     *
     * @param fileMap 需要被压缩的文件
     * @return 压缩后的文件流
     * @throws UtilException 异常
     */
    public static ByteArrayInputStream zip(Map<String, ? extends InputStream> fileMap) throws UtilException, IOException {
        return zip(fileMap, null);
    }

    /**
     * zip打包
     *
     * @param fileMap 需要被压缩的文件
     * @param charset 编码
     * @return 压缩后的文件流
     * @throws UtilException 异常
     */
    public static ByteArrayInputStream zip(Map<String, ? extends InputStream> fileMap, Charset charset) throws UtilException, IOException {

        assert fileMap != null;
        assert !fileMap.isEmpty();

        //只保留空文件夹及文件并按父子关系排序
        List<String> paths = filePathClear(fileMap);
        ArrayList<InputStream> ins = new ArrayList<>(fileMap.size());
        paths.forEach(path -> ins.add(fileMap.get(path)));

        if (charset == null) {
            charset = UTF_8;
        }
        if (paths.size() != ins.size()) {
            throw new IllegalArgumentException("Path length is not equals to InputStream length !");
        }

        //创建输出流接受zip文件信息
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        //zip写入输出流
        ZipOutputStream out = new ZipOutputStream(byteArrayOutputStream, charset);
        try {
            for (int i = 0; i < paths.size(); i++) {
                addZipFile(ins.get(i), paths.get(i), out);
            }
        } finally {
            //关闭流
            ins.forEach(IoUtil::close);
            IoUtil.close(out);
        }
        //获取zip文件的内容
        byte[] zipBytes = byteArrayOutputStream.toByteArray();
        return new ByteArrayInputStream(zipBytes);
    }

    /**
     * 添加文件流到压缩包，不关闭输入流
     *
     * @param in   需要压缩的输入流
     * @param path 压缩的路径 如:"/upgrade.json"
     * @param out  压缩文件存储对象
     * @throws UtilException IO异常
     */
    private static void addZipFile(InputStream in, String path, ZipOutputStream out) throws UtilException {
        try {
            //处理path,解决部分压缩软件解压出现一级空目录
            if (path.startsWith(SEPARATOR)) {
                path = path.substring(1, path.length());
            }
            //创建文件或文件夹
            out.putNextEntry(new ZipEntry(path));
            //不为空为文件，为空则是文件夹
            if (null != in) {
                //写入数据
                IoUtil.copy(in, out);
            }
        } catch (Exception e) {
            throw new UtilException(e);
        } finally {
            try {
                out.closeEntry();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ZipUtilExt.setTempFolder("D://zip_temp");
        FileInputStream fileInputStream = IoUtil.toStream(new File("D://新建文件夹 (3).zip"));
        Map<String, ByteArrayInputStream> streamMap = unzip(fileInputStream);

        streamMap.put("/empty/", null);
        streamMap.put("/folder/1.txt", IoUtil.toUtf8Stream("test content"));

        FileUtil.del("D://new.zip");
        InputStream zip = zip(streamMap);
        FileUtil.writeFromStream(zip, "D://new.zip");
    }

    /**
     * 解压zip文件
     *
     * @param stream zip文件流，包含编码信息
     * @return 解压的文件集合 key为路径如:"/upgrade.json" value为InputStream
     * @throws UtilException 异常
     */
    public static Map<String, ByteArrayInputStream> unzip(InputStream stream) {
        return unzip(stream, UTF_8);
    }

    /**
     * 解压zip文件
     *
     * @param stream   zip文件流，包含编码信息
     * @param encoding 编码
     * @return 解压的文件集合 key为路径如:"/upgrade.json" value为InputStream
     * @throws UtilException 异常
     */
    public static Map<String, ByteArrayInputStream> unzip(InputStream stream, String encoding) {
        return unzip(stream, Charset.forName(encoding));
    }

    /**
     * 解压zip文件
     *
     * @param stream  zip文件流，包含编码信息
     * @param charset 编码
     * @return 解压的文件集合 key为路径如:"/upgrade.json" value为InputStream
     * @throws UtilException 异常
     */
    public static Map<String, ByteArrayInputStream> unzip(InputStream stream, Charset charset) {

        //缓存为临时文件
        String tempPath = (TEMP_FOLDER == null ? System.getProperty("jeplus.webapp") + "/zip_temp/%s.zip" : TEMP_FOLDER + "/%s.zip");
        String tempFilePath = String.format(tempPath, System.currentTimeMillis());
        FileUtil.writeFromStream(stream, tempFilePath);
        IoUtil.close(stream);
        File tempFile = new File(tempFilePath);

        Map<String, ByteArrayInputStream> fileMap = new LinkedHashMap<>();
        try {
            ZipFile zipFile = new ZipFile(tempFile, charset);
            final Enumeration<ZipEntry> em = (Enumeration<ZipEntry>) zipFile.entries();
            ZipEntry zipEntry;
            while (em.hasMoreElements()) {
                //获取文件或文件夹
                zipEntry = em.nextElement();
                String name = zipEntry.getName();
                if (!name.startsWith(SEPARATOR)) {
                    name = SEPARATOR + name;
                }
                if (!zipEntry.isDirectory()) {
                    //文件
                    fileMap.put(name, IoUtil.toStream(IoUtil.readBytes(zipFile.getInputStream(zipEntry))));
                } else {
                    //文件夹
                    fileMap.put(name, null);
                }
            }

            //只保留空文件夹和文件
            filePathClear(fileMap);

        } catch (IllegalArgumentException e) {
            //出现此错误大多为编码错误，平台默认为UTF-8，此时将编码改为GBK重试。
            if (e.getMessage().contains(EXCEPTION_MALFORMED) && !GBK.equals(charset)) {
                try {
                    return unzip(new FileInputStream(tempFile), GBK);
                } catch (FileNotFoundException fileNotFoundException) {
                    throw new RuntimeException(fileNotFoundException.getCause());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            FileUtil.del(tempFile);
        }
        return fileMap;
    }

    /**
     * 只保留空文件夹和文件的path
     *
     * @param fileMap 文件目录map
     */
    public static List<String> filePathClear(Map<String, ?> fileMap) {

        //排序后父级目录在前
        List<String> paths = fileMap.keySet().stream().sorted().collect(Collectors.toList());
        String prevPath = "";
        for (String path : paths) {
            if (path.contains(prevPath)) {
                //删除父级目录
                fileMap.remove(prevPath);
            }
            prevPath = path;
        }
        return fileMap.keySet().stream().sorted().collect(Collectors.toList());
    }

    /**
     * 设置临时文件缓存位置
     *
     * @param tempFolder 临时文件目录
     */
    public static void setTempFolder(String tempFolder) {
        TEMP_FOLDER = tempFolder;
    }

//    /**
//     * 解压zip文件
//     *
//     * @param stream                               zip文件流，包含编码信息
//     * @param encoding                             编码
//     * @param useUnicodeExtraFields                参见 ZipArchiveInputStream 构造器
//     * @param allowStoredEntriesWithDataDescriptor 参见 ZipArchiveInputStream 构造器
//     * @return 解压的文件集合 key为路径如:"/upgrade.json" value为InputStream
//     * @throws UtilException IO异常
//     */
//    public static Map<String, ByteArrayInputStream> unzip(InputStream stream, String encoding, boolean useUnicodeExtraFields, boolean allowStoredEntriesWithDataDescriptor) throws UtilException {
//        ZipArchiveInputStream zipStream = new ZipArchiveInputStream(stream, encoding, false, true);
//        Map<String, ByteArrayInputStream> fileMap = new HashMap<>();
//        try {
//            ArchiveEntry zipEntry = null;
//            File outItemFile = null;
//            while (null != (zipEntry = zipStream.getNextEntry())) {
//                if (!zipEntry.isDirectory()) {
//                    byte[] bytes = IoUtil.readBytes(zipStream);
//                    String name = zipEntry.getName();
//                    //添加"/"
//                    if (!name.startsWith(separator)) {
//                        name = separator + name;
//                    }
//                    fileMap.put(name, IoUtil.toStream(bytes));
//                }
//            }
//        } catch (IOException e) {
//            throw new UtilException(e);
//        } finally {
//            IoUtil.close(zipStream);
//            IoUtil.close(stream);
//        }
//        return fileMap;
//    }

}